/**
 * @class 行星实体
 * @author DuskyHuang 圣子
 * @description 对外视看做为一颗行星对象，对内管理自身行为和表现。
*/

import { _decorator, Node, renderer, MeshRenderer, Color, Material } from 'cc';
import { core, utils } from 'cfw';
import { Res } from 'const';
import { calcLensBias, calcTiling } from 'algorithms';
import { getPresetPlanetMatInst, loadCelTexture, preloadCelTexture, setMatColor, setMatScaleVec3 } from 'helpers';
import { Orbiter } from './orbiter';
const { ccclass, type } = _decorator;
const { group } = utils;

@ccclass('cocosmos.Planet')
export class Planet extends Orbiter {

    declare protected _config: planetConfig;
    public get config(): planetConfig { return this._config as planetConfig; }
    protected readonly rotatable: boolean = true;
    protected readonly important: boolean = true;
    public readonly cat = 'planet';
    protected get LODValves(): num.pct[] { return PLANET_LOD_VALVES };

    @group("自转参数.A", "表面风速", ["unit", "deg/s"]) public windVelocity: num.deg = 1;
    protected cloudAngle: num.deg = 0;

    @type([Material]) public presetMaterials: Material[] = [];

    protected init(): void {
        super.init();
        this._atmosphere = this._lod0!.getChildByName('Atmosphere')!;
        this._clouds = this._lod0!.getChildByName('Clouds')!;
        this._ring = this._lod0!.getChildByName('Ring');
        this._meshAtm = this._atmosphere.getComponent(MeshRenderer);
        this._mesh1 = this._lod1!.getComponent(MeshRenderer);
        this._mesh2 = this._lod2!.getComponent(MeshRenderer);
        this._mesh3 = this._lod3!.getComponent(MeshRenderer);
    }

    protected step(span: time.sec): void {
        super.step(span);
        this.cloudAngle += this.windVelocity * span;
        this.faceToSun(this._lod2);
    }

    protected rotate(span: time.sec): void {
        super.rotate(span);
        if (this._clouds.active) this._clouds.setRotationFromEuler(0, this.cloudAngle, 0);
    }

    public configure(): void {
        this._loadSurfaceParams();
        this._loadLODParams();
        this._bias = calcLensBias(this.config.diam, SMALLEST, BIGGEST, NEAREST, FARTHEST);
    }

    private _sourcePreloaded: boolean = false;
    public preloadRes(): void {
        if (this._sourcePreloaded) return;
        this._sourcePreloaded = true;
        const name = this._config.name as planet;
        preloadCelTexture(name, 'albedo', true);
        const { land, atmosphere, reflect } = this._config.surface!;
        if (land?.mountains) preloadCelTexture(name, 'normal', true);
        if (reflect?.night) preloadCelTexture(name, 'emissive', true);
        if (atmosphere?.clouds) preloadCelTexture(name, 'clouds', true);
    }

    private _loadSurfaceParams(): void {
        if (USE_PRESET) this._loadPresetMaterialInst();
        this._mat0 = this._lod0!.getComponent(MeshRenderer)!.getMaterialInstance(0);
        this._pass0 = this._mat0.passes[0];
        this._solarPos = this._pass0.getHandle('solarPos');
        const { land, ring } = this._config.surface;
        if (land.tint) {
            const { r, g, b, a } = Color.fromHEX(col, land.tint ?? '#FFFFFF');
            this._tint = [r, g, b, a];
        }
        if (ring) this._lod1.getChildByName('Ring').active = true;
    }

    private _loadLODParams(): void {
        const { lod } = this._config;
        this._tiling = calcTiling(lod, CELESTIAL_GRIDS);
    }

    protected syncInstancing(): void {
        this._pass0.setUniform(this._solarPos, this._belong.node.worldPosition);
        this._atmosphere.active && this._meshAtm.setInstancedAttribute('instanced_color', this._atmColor);
        this._clouds.active && this._pass0Cloud && this._pass0Cloud.setUniform(this._solarPosCloud, this._belong.node.worldPosition);
        this._mesh1.setInstancedAttribute('instanced_uv', this._tiling);
        this._mesh2.setInstancedAttribute('instanced_uv', this._tiling);
        this._mesh3.setInstancedAttribute('instanced_color', this._tint);
    }

    protected onLod0Initiated(): void {
        const name = this._config.name as planet;
        const low = name.toLocaleLowerCase();
        // surface land map
        loadCelTexture(this._mat0, name, 'mainTexture', 'albedo', true);
        const { land, atmosphere, reflect, ring } = this._config.surface!;
        const { brightness = 1, mountains, wind } = land;
        setMatScaleVec3(this._mat0, 'albedoScale', brightness * 1.5);
        if (mountains) {
            loadCelTexture(this._mat0, name, 'normalMap', 'normal', true);
            this._mat0.setProperty('normalStrength', Math.clamp(mountains.strength ?? 1, 0, 5));
        }
        if (wind) {
            const tex = core.res.getSheetFrame(Res.Sheets.PRESET, `${low}-wind`);
            this._mat0.setProperty(`windMap`, tex.texture);
        }
        if (notEmpty(atmosphere)) {
            this._atmosphere.active = true;
            const { scale, color, thickness, clouds } = atmosphere;
            const s = scale ?? DEFAULT_ATM_SCALE;
            this._atmosphere.setScale(s, s, s);
            const { r, g, b, a } = Color.fromHEX(col, color ?? '#FFFFFF');
            this._atmColor = [r, g, b, a];
            /** @notice don't get instance material like next line, atmosphere is setup by 'instanced_color' and unified set params by shared material */
            // setMatColor(this._atmosphere.getComponent(MeshRenderer)?.material!, 'atmColor', color, thickness);
            if (notEmpty(clouds)) {
                const { color, strength = 1, speed = 0 } = clouds;
                const matC = this._clouds.getComponent(MeshRenderer)?.material!;
                this._pass0Cloud = matC.passes[0];
                this._solarPosCloud = this._pass0Cloud.getHandle('solarPos');
                loadCelTexture(matC, name, 'mainTexture', 'clouds', true);
                setMatColor(matC, 'cloudColor', color, thickness);
                setMatScaleVec3(matC, 'albedoScale', strength);
                this.windVelocity = speed;
                this._clouds.active = true;
            } else this._clouds.active = false;
        } else this._atmosphere.active = false;
        if (notEmpty(reflect)) {
            const { color, strength, night } = reflect;
            setMatColor(this._mat0, 'fresnelColor', color, strength);
            if (night) {
                loadCelTexture(this._mat0, name, 'emissiveMap', 'emissive', true);
                setMatScaleVec3(this._mat0, 'emissiveScale', night.strength);
            }
        }
        if (ring) {
            const matR = this._ring.getComponent(MeshRenderer)?.material!;
            const tex = core.res.getSheetFrame(Res.Sheets.PRESET, `${low}-ring`);
            if (matR && tex) matR.setProperty(`mainTexture`, tex.texture);
            else console.warn('lost ring texture for:', name);
            this._ring.active = true;
        } else this._ring.active = false;
        if (!USE_PRESET) {
            if (mountains || reflect.night || wind || reflect.sea) {
                this._mat0.recompileShaders({
                    USE_DUP_MOUNTAIN_MAP: !!mountains,
                    USE_DUP_NIGHT_LIGHT_MAP: !!reflect.night,
                    USE_SURFACE_WIND_MAP: !!wind,
                    USE_AUTO_SEA_REFLECTION: !!reflect.sea,
                });
            }
        }
    }

    private _loadPresetMaterialInst(): void {
        /** use presets to decrease shader compiling time **/
        const preset = getPresetPlanetMatInst(this._config.name as planet);
        const mat = this.presetMaterials.find(mat => mat.name === preset);
        const mesh = this._lod0.getComponent(MeshRenderer);
        mesh.setMaterialInstance(mat, 0);
    }

    private _atmosphere?: Node;
    private _clouds?: Node;
    private _ring?: Node;
    private _mat0: renderer.MaterialInstance;
    private _pass0: renderer.Pass;
    private _solarPos: handle;
    private _pass0Cloud: renderer.Pass;
    private _solarPosCloud: handle;
    private _meshAtm: MeshRenderer;
    private _mesh1: MeshRenderer;
    private _mesh2: MeshRenderer;
    private _mesh3: MeshRenderer;
    private _tiling: tiling;
    private _atmColor: vec4;
    private _tint: vec4 = [255, 255, 255, 255];
}

const { Sheets: { PLANETS_LOD_GIRDS: CELESTIAL_GRIDS } } = Res;
const col = new Color;
const DEFAULT_ATM_SCALE = 1.2;
const PLANET_LOD_VALVES = [3, 1.5, 0.5, 0.2];

const SMALLEST = 4.878e-5;  // diameter of Mercury (ykm)
const BIGGEST = 139.822e-5; // diameter of Jupiter (ykm)
const NEAREST = 0.0;
const FARTHEST = 0.2;
const USE_PRESET: boolean = true;